Building a Privacy-Preserving ML Service with Go and Intel SGX
February 4, 2024Building a Privacy-Preserving ML Service with Go and Intel SGX
Or: How I Learned to Stop Worrying and Love the Enclave
The Trust Problem Nobody Talks About
Picture this: You're a hospital with patient data that could help train a life-saving disease prediction model. There's a company with an amazing ML model that could use your data. Sounds like a match made in heaven, right?
Wrong. It's actually a match made in legal-nightmare-land.
You can't share your patient data (hello, HIPAA). They can't share their model (goodbye, competitive advantage). You're both standing there, arms crossed, like two kids who won't share their toys.
This is the "dual privacy problem," and it's been haunting the ML industry like that one bug you swear you fixed three sprints ago.
Enter Confidential Computing (Dramatic Music)
What if I told you there's a way for both parties to use each other's sensitive assets without either one actually seeing them?
No, I'm not talking about blockchain. Put down the whitepaper.
I'm talking about Intel SGX (Software Guard Extensions) – a technology that creates secure "enclaves" in your CPU where code and data are encrypted even while being processed. Not just at rest. Not just in transit. But actually while the CPU is crunching numbers.
It's like having a tiny, paranoid vault inside your processor that doesn't trust anyone – not the operating system, not the hypervisor, not even the person who owns the server. The only thing it trusts is math. And honestly? Same.
What We're Building
Today, we're building ConfidentialML – a privacy-preserving XGBoost inference service using:
- Go – because we're not animals
- EGo – a framework that makes SGX development actually pleasant
- XGBoost – the ML algorithm that won't stop winning Kaggle competitions
Here's the magic trick:
- Model Owner uploads their proprietary XGBoost model → it gets sealed (encrypted) inside the enclave
- Data Owner sends their sensitive features → processed only inside the enclave
- Enclave runs inference → returns only the prediction
- Neither party ever sees the other's sensitive data
┌─────────────────────────────────────────────────────────────┐│ SGX Enclave ││ (The Trust Zone™) ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││ │ Sealed │ │ XGBoost │ │ Attestation │ ││ │ Model │───▶│ Inference │◀───│ Service │ ││ │ 🔒 │ │ 🧠 │ │ 📜 │ ││ └─────────────┘ └─────────────┘ └─────────────┘ │└─────────────────────────────────────────────────────────────┘ ▲ │ │ Model Owner Data Owner Verifier (can't see data) (can't see model) (trusts math)
The Three Pillars of Confidential Computing
Before we dive into code, let's understand the three superpowers that SGX gives us:
1. Runtime Encryption 🔐
Normal encryption protects data at rest (on disk) and in transit (over the network). But what about when you're actually using the data? That's when it's most vulnerable – sitting there in RAM, naked and exposed.
SGX encrypts data in memory. Even if someone dumps your server's RAM (looking at you, cold boot attacks), they'll just see gibberish. The decryption keys never leave the CPU.
2. Sealing 📦
"Sealing" is SGX's way of saying "encrypt this data so only this specific enclave on this specific CPU can decrypt it."
When we seal our ML model, we're essentially putting it in a lockbox that can only be opened by our exact code running on that exact machine. Try to open it anywhere else? Nope. Different code? Nope. Same code but different machine? Still nope.
It's like a password manager, except the password is "being the right code on the right hardware."
3. Remote Attestation 🔍
Here's where it gets really cool. How does a client know they're talking to a genuine SGX enclave running the expected code?
Remote attestation. The enclave can generate a cryptographic proof that says:
- "I am running inside a genuine Intel SGX enclave"
- "My code has this exact hash (MRENCLAVE)"
- "I was signed by this key (MRSIGNER)"
The client can verify this proof before sending any sensitive data. It's like checking someone's ID, except the ID is mathematically unforgeable.
Let's Build This Thing
Project Structure
confidential-ml/├── cmd/│ ├── server/ # The enclave server│ └── client/ # Demo client with attestation├── internal/│ ├── api/ # HTTP handlers│ ├── attestation/ # Remote attestation magic│ ├── model/ # XGBoost inference│ └── sealing/ # SGX sealing service├── scripts/│ └── train_model.py # Train our demo model├── enclave.json # EGo configuration└── Makefile # Because typing is hard
Step 1: The Sealing Service
First, let's build the service that encrypts our model using SGX sealing:
// internal/sealing/service.gopackage sealing
import ( "github.com/edgelesssys/ego/ecrypto")
// Service provides sealing operations using EGo's ecryptotype Service struct { simulationMode bool}
// Seal encrypts data using the enclave's product key.// The sealed data can only be decrypted by the same enclave binary.func (s *Service) Seal(plaintext []byte) ([]byte, error) { if s.simulationMode { // In simulation mode, use mock sealing for development return s.mockSeal(plaintext), nil }
// The real deal - uses hardware-derived keys return ecrypto.SealWithProductKey(plaintext, nil)}
// Unseal decrypts previously sealed data.// This will fail if called from a different enclave or platform.func (s *Service) Unseal(ciphertext []byte) ([]byte, error) { if s.simulationMode { return s.mockUnseal(ciphertext), nil }
return ecrypto.Unseal(ciphertext, nil)}
The beauty here is that SealWithProductKey uses a key derived from:
- The enclave's identity (MRSIGNER)
- The CPU's unique key
- Some cryptographic fairy dust
Try to unseal this data on a different machine or with different code? The CPU will just shrug and say "I don't know her."
Step 2: The Model Manager
Now let's handle loading and managing our XGBoost model:
// internal/model/manager.gopackage model
import ( "github.com/Elvenson/xgboost-go" "github.com/example/confidential-ml/internal/sealing")
type Manager struct { ensemble *inference.Ensemble sealing *sealing.Service modelPath string // ... other fields}
// LoadModel parses and loads an XGBoost model from JSONfunc (m *Manager) LoadModel(jsonData []byte) error { // Load the model using xgboost-go ensemble, err := xgboost.LoadXGBoostFromJSONBytes( jsonData, "", // No feature map numClasses, // 3 for Iris maxDepth, // Tree depth activationFn, // Softmax for classification ) if err != nil { return err }
m.ensemble = ensemble m.rawJSON = jsonData return nil}
// SaveSealed seals and persists the model to diskfunc (m *Manager) SaveSealed() error { return m.sealing.SealToFile(m.rawJSON, m.modelPath)}
// LoadSealed loads a previously sealed modelfunc (m *Manager) LoadSealed() error { jsonData, err := m.sealing.UnsealFromFile(m.modelPath) if err != nil { return err } return m.LoadModel(jsonData)}
When the model owner uploads their model, we:
- Parse it to make sure it's valid
- Seal it using SGX
- Write the sealed blob to disk
The file on disk is completely useless without the enclave. You could email it to your competitors and they'd just see random bytes. (Please don't actually do this. Your security team will have questions.)
Step 3: The Inference Engine
Here's where the magic happens – running predictions without exposing the model:
// internal/model/inference.gopackage model
type PredictionResult struct { Value float64 Probability map[string]float64 ModelType ModelType}
// Predict runs inference on a feature vectorfunc (e *InferenceEngine) Predict(features []float64) (*PredictionResult, error) { // Get the model (it's decrypted only in enclave memory) ensemble, err := e.manager.GetModel() if err != nil { return nil, errors.New("no model loaded") }
// Validate feature count expectedFeatures := e.manager.GetNumFeatures() if len(features) != expectedFeatures { return nil, fmt.Errorf( "feature mismatch: expected %d, got %d", expectedFeatures, len(features), ) }
// Run XGBoost prediction input := createSparseMatrix(features) probs, err := ensemble.PredictProba(input) if err != nil { return nil, err }
// Return only the prediction, not the model internals return &PredictionResult{ Value: findMaxClass(probs), Probability: formatProbabilities(probs), ModelType: ModelTypeClassification, }, nil}
Notice what we're not doing:
- We're not logging the input features (privacy!)
- We're not exposing model weights (IP protection!)
- We're not storing the data anywhere (compliance!)
The features come in, the prediction goes out, and everything in between stays encrypted in enclave memory.
Step 4: Remote Attestation
Now for the trust verification. How does a client know they're talking to the real deal?
// internal/attestation/service.gopackage attestation
import ( "github.com/edgelesssys/ego/enclave")
type Report struct { RawReport []byte UniqueID []byte // MRENCLAVE - hash of enclave code SignerID []byte // MRSIGNER - hash of signing key ProductID uint16 SecurityVer uint16 IsSimulation bool}
// GetReport generates an attestation reportfunc (s *Service) GetReport(userData []byte) (*Report, error) { if s.isSimulation { return s.getSimulatedReport(userData) }
// Get the self report from the enclave selfReport, err := enclave.GetSelfReport() if err != nil { return nil, err }
// Get remote report with user data remoteReport, err := enclave.GetRemoteReport(userData) if err != nil { return nil, err }
return &Report{ RawReport: remoteReport, UniqueID: selfReport.UniqueID, SignerID: selfReport.SignerID, ProductID: extractProductID(selfReport), SecurityVer: uint16(selfReport.SecurityVersion), IsSimulation: false, }, nil}
The UniqueID (MRENCLAVE) is particularly important – it's a hash of the enclave's code. If someone modifies even a single byte of the code, the hash changes, and clients will reject the connection.
It's like a tamper-evident seal, except instead of "void if removed," it's "void if you try anything funny."
Step 5: The API Layer
Let's wire everything together with a REST API:
// internal/api/handler.gopackage api
// POST /model - Upload XGBoost modelfunc (h *Handler) handleModelUpload(w http.ResponseWriter, r *http.Request, requestID string) { body, err := io.ReadAll(r.Body) if err != nil { h.writeError(w, 400, "PARSE_ERROR", "Failed to read request", requestID) return }
// Load the model if err := h.modelManager.LoadModel(body); err != nil { h.writeError(w, 400, "INVALID_MODEL", err.Error(), requestID) return }
// Seal and persist if err := h.modelManager.SaveSealed(); err != nil { h.writeError(w, 500, "SEALING_ERROR", "Failed to persist model", requestID) return }
// Return model info (but not the model itself!) info, _ := h.modelManager.GetModelInfo() json.NewEncoder(w).Encode(info)}
// POST /predict - Run inferencefunc (h *Handler) handlePredict(w http.ResponseWriter, r *http.Request, requestID string) { var req PredictRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.writeError(w, 400, "PARSE_ERROR", "Invalid JSON", requestID) return }
// Run inference (features never logged or persisted!) result, err := h.inference.Predict(req.Features) if err != nil { h.writeError(w, 400, "INFERENCE_ERROR", err.Error(), requestID) return }
json.NewEncoder(w).Encode(result)}
// GET /attestation - Get attestation reportfunc (h *Handler) handleAttestation(w http.ResponseWriter, r *http.Request, requestID string) { report, err := h.attestation.GetReport(nil) if err != nil { h.writeError(w, 500, "ATTESTATION_ERROR", "Failed to generate report", requestID) return }
json.NewEncoder(w).Encode(AttestationResponse{ UniqueID: hex.EncodeToString(report.UniqueID), SignerID: hex.EncodeToString(report.SignerID), ProductID: report.ProductID, SecurityVer: report.SecurityVer, IsSimulation: report.IsSimulation, })}
Step 6: The Demo Client
Finally, let's build a client that demonstrates the full flow:
// cmd/client/main.gofunc (c *Client) RunDemo() { fmt.Println("╔════════════════════════════════════════════════════════════╗") fmt.Println("║ ConfidentialML - Private XGBoost Inference Demo ║") fmt.Println("╚════════════════════════════════════════════════════════════╝")
// Step 1: Health check fmt.Println("📋 Step 1: Checking server health...") health, _ := c.GetHealth() fmt.Printf(" ✅ Server is %s\n", health.Status) fmt.Printf(" 🔒 Enclave mode: %s\n", health.EnclaveMode)
// Step 2: Verify attestation fmt.Println("🔐 Step 2: Verifying enclave attestation...") attest, _ := c.GetAttestation() fmt.Printf(" 📜 Unique ID: %s...\n", attest.UniqueID[:16]) fmt.Printf(" ✍️ Signer ID: %s...\n", attest.SignerID[:16])
if attest.IsSimulation { fmt.Println(" ⚠️ Running in SIMULATION mode") } else { fmt.Println(" ✅ Running in real SGX enclave") }
// Step 3: Run inference fmt.Println("🧠 Step 3: Running private inference...") fmt.Println(" Sending features: [5.1, 3.5, 1.4, 0.2]")
result, _ := c.Predict([]float64{5.1, 3.5, 1.4, 0.2})
fmt.Printf(" ✅ Prediction: %.0f\n", result.Prediction) fmt.Println(" 📈 Probabilities:", result.Probability)
fmt.Println("\n✅ Demo complete!") fmt.Println(" The model owner never saw your data.") fmt.Println(" You never saw the model weights.") fmt.Println(" Privacy preserved! 🎉")}
Running the Demo
Let's see this thing in action!
1. Train a Demo Model
# Create virtual environment and install dependenciespython3 -m venv venv./venv/bin/pip install xgboost scikit-learn numpy
# Train the model./venv/bin/python scripts/train_model.py
Output:
============================================================ConfidentialML - Demo Model Training============================================================
📊 Loading Iris dataset... Features: ['f0', 'f1', 'f2', 'f3'] Classes: ['setosa', 'versicolor', 'virginica'] Samples: 150
🤖 Training XGBoost classifier... Trained 50 rounds
📊 Model Evaluation: Accuracy: 1.0000
💾 Exporting model to data/iris_model.json... Model saved (64,522 bytes)
✅ Model training complete!
2. Start the Server
# Build and run in simulation mode (no SGX hardware needed)go build -o confidential-ml ./cmd/server./confidential-ml
Output:
{"level":"INFO","msg":"Starting ConfidentialML server","port":"8080"}{"level":"WARN","msg":"Running in SIMULATION mode - for development only"}{"level":"INFO","msg":"Server listening","addr":":8080"}
3. Upload the Model
curl -X POST http://localhost:8080/model \ -H "Content-Type: application/json" \ --data-binary @data/iris_model.json
Response:
{ "num_features": 4, "num_trees": 150, "model_type": "classification", "num_classes": 3}
The model is now sealed inside the enclave. The file on disk (sealed_model.bin) is encrypted and useless outside this specific enclave.
4. Run Inference
curl -X POST http://localhost:8080/predict \ -H "Content-Type: application/json" \ -d '{"features": [5.1, 3.5, 1.4, 0.2]}'
Response:
{ "prediction": 0, "probability": { "0": 0.9939, "1": 0.0045, "2": 0.0017 }, "model_type": "classification"}
The model predicted class 0 (setosa) with 99.39% confidence. And at no point did:
- The server log your input features
- You see the model weights
- Anyone have access to unencrypted data outside the enclave
5. Run the Full Demo
go build -o client ./cmd/client./client --mode demo
Output:
╔════════════════════════════════════════════════════════════╗║ ConfidentialML - Private XGBoost Inference Demo ║╚════════════════════════════════════════════════════════════╝
📋 Step 1: Checking server health... ✅ Server is healthy 🔒 Enclave mode: simulation 📦 Model loaded: true
🔐 Step 2: Verifying enclave attestation... 📜 Unique ID (MRENCLAVE): 3130be0a0da1ef13... ✍️ Signer ID (MRSIGNER): 0530a8fe59fed475... 🏭 Product ID: 1 🔢 Security Version: 1 ⚠️ Running in SIMULATION mode (development only)
🧠 Step 3: Running private inference... Sending Iris flower features: [5.1, 3.5, 1.4, 0.2] (These features stay encrypted in transit and memory) ✅ Prediction: 0 📊 Model type: classification 📈 Class probabilities: - Class 0: 0.9939 - Class 1: 0.0045 - Class 2: 0.0017
════════════════════════════════════════════════════════════✅ Demo complete! The model owner never saw your data, and you never saw the model weights. Privacy preserved!════════════════════════════════════════════════════════════
6. See What Happens When Trust Fails
./client --mode trust-failure
Output:
╔════════════════════════════════════════════════════════════╗║ Trust Failure Demonstration ║╚════════════════════════════════════════════════════════════╝
This demo shows what happens when a client has incorrectexpectations about the enclave identity.
📜 Actual enclave Unique ID: 9282306c0dd15186...
❌ Client expects Unique ID: 0000000000000000...
🚫 TRUST VERIFICATION FAILED!
The enclave's identity does not match what we expected. This could mean: • The server is running different code than expected • The server is not running in a genuine SGX enclave • A man-in-the-middle attack is occurring
⛔ A security-conscious client would REFUSE to send sensitive data in this situation!
This is the beauty of remote attestation – if anything is off, the client knows immediately.
Real-World Applications
This isn't just a cool demo. Here are some actual use cases:
Healthcare 🏥
- Hospital sends patient vitals → gets disease risk prediction
- Patient data never leaves the hospital's control
- Model IP stays protected
- HIPAA compliance maintained
Finance 💰
- Bank sends customer data → gets credit score
- Proprietary scoring model stays secret
- Customer data isn't exposed to model provider
- Regulatory requirements satisfied
Insurance 📋
- Insurer gets risk assessment
- Pricing algorithm stays confidential
- Customer data protected
- Both parties happy (rare in insurance)
Federated Learning 🤝
- Multiple parties contribute to model training
- No one sees anyone else's data
- Model improves without data sharing
- Everyone wins
Limitations and Considerations
Before you go rewriting your entire ML infrastructure, some caveats:
Performance
SGX enclaves have limited memory (typically 128MB-256MB). Large models might not fit. There's also some overhead for encryption/decryption, though it's usually negligible for inference.
Side-Channel Attacks
SGX isn't perfect. Researchers have found side-channel attacks (Spectre, Meltdown, etc.) that can leak information. Intel keeps patching these, but it's an ongoing cat-and-mouse game.
Hardware Requirements
Real SGX requires Intel CPUs with SGX support. The good news is that EGo's simulation mode lets you develop without the hardware. The bad news is that simulation mode doesn't provide actual security guarantees.
Complexity
There's a learning curve. You need to understand enclaves, attestation, sealing, and how they all fit together. Hopefully this blog helped with that!
Conclusion
We built a privacy-preserving ML inference service that solves the dual privacy problem:
- Model owners can deploy their proprietary models without exposing the weights
- Data owners can get predictions without exposing their sensitive data
- Both parties can verify they're dealing with a genuine, unmodified enclave
The code is available on GitHub (link in the description), and you can run it in simulation mode without any special hardware.
Confidential computing is still a relatively new field, but it's growing fast. As more organizations deal with sensitive data and ML, solutions like this will become increasingly important.
Now if you'll excuse me, I need to go seal some secrets. 🔐
Resources
- EGo Documentation
- Intel SGX Overview
- Confidential Computing Consortium
- XGBoost-Go Library
- Project Source Code
Did you find this helpful? Have questions about confidential computing? Drop a comment below or find me on Twitter [@yourhandle]. And if you're building something cool with SGX, I'd love to hear about it!